package com.impossibl.stencil.engine.internal; import static com.google.common.base.Strings.nullToEmpty; import static com.impossibl.stencil.api.Callable.ALL_PARAM_NAME; import static com.impossibl.stencil.api.Preparable.ALL_BLOCK_NAME; import static com.impossibl.stencil.api.Preparable.UNNAMED_BLOCK_NAME; import static com.impossibl.stencil.engine.internal.Contexts.mode; import static com.impossibl.stencil.engine.internal.Contexts.name; import static com.impossibl.stencil.engine.internal.Contexts.value; import static com.impossibl.stencil.engine.internal.ExtensionMethodManager.getExtensionMethod; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.RuleNode; import org.apache.commons.beanutils.ConstructorUtils; import org.apache.commons.beanutils.MethodUtils; import org.apache.commons.beanutils.PropertyUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.google.common.base.Splitter; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ContiguousSet; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.impossibl.stencil.api.Block; import com.impossibl.stencil.api.Callable; import com.impossibl.stencil.api.GlobalScope; import com.impossibl.stencil.api.Preparable; import com.impossibl.stencil.engine.ExecutionException; import com.impossibl.stencil.engine.ExecutionLocation; import com.impossibl.stencil.engine.InvocationException; import com.impossibl.stencil.engine.ParseException; import com.impossibl.stencil.engine.StencilEngine; import com.impossibl.stencil.engine.UndefinedTypeException; import com.impossibl.stencil.engine.UndefinedVariableException; import com.impossibl.stencil.engine.parsing.ParamOutputBlockMode; import com.impossibl.stencil.engine.parsing.StencilParser.AssignOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.AssignmentContext; import com.impossibl.stencil.engine.parsing.StencilParser.AssignmentStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.BinaryExpressionContext; import com.impossibl.stencil.engine.parsing.StencilParser.BlockDeclContext; import com.impossibl.stencil.engine.parsing.StencilParser.BlockStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.BooleanLiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.BreakOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.BreakStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.CallSelectorContext; import com.impossibl.stencil.engine.parsing.StencilParser.CallableInvocationContext; import com.impossibl.stencil.engine.parsing.StencilParser.CallableSignatureContext; import com.impossibl.stencil.engine.parsing.StencilParser.ContinueOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.ContinueStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.DeclarationOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.DeclarationStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.DynamicOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.ExportDefinitionContext; import com.impossibl.stencil.engine.parsing.StencilParser.ExpressionContext; import com.impossibl.stencil.engine.parsing.StencilParser.ExpressionOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.ExpressionStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.FloatingLiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.ForeachOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.ForeachStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.FunctionDefinitionContext; import com.impossibl.stencil.engine.parsing.StencilParser.HeaderContext; import com.impossibl.stencil.engine.parsing.StencilParser.IfOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.IfStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.IncludeOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.IndexSelectorContext; import com.impossibl.stencil.engine.parsing.StencilParser.InstanceExpressionContext; import com.impossibl.stencil.engine.parsing.StencilParser.IntegerLiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.LValueRefContext; import com.impossibl.stencil.engine.parsing.StencilParser.ListLiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.LiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.LiteralExpressionContext; import com.impossibl.stencil.engine.parsing.StencilParser.MacroDefinitionContext; import com.impossibl.stencil.engine.parsing.StencilParser.MapLiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.MemberIndexSelectorContext; import com.impossibl.stencil.engine.parsing.StencilParser.MemberSelectorContext; import com.impossibl.stencil.engine.parsing.StencilParser.MethodCallSelectorContext; import com.impossibl.stencil.engine.parsing.StencilParser.NamedOutputBlockContext; import com.impossibl.stencil.engine.parsing.StencilParser.NamedValueContext; import com.impossibl.stencil.engine.parsing.StencilParser.NullLiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.NumberLiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.OutputBlockContext; import com.impossibl.stencil.engine.parsing.StencilParser.OutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.ParameterDeclContext; import com.impossibl.stencil.engine.parsing.StencilParser.ParenExpressionContext; import com.impossibl.stencil.engine.parsing.StencilParser.PrepareInvocationContext; import com.impossibl.stencil.engine.parsing.StencilParser.PrepareSignatureContext; import com.impossibl.stencil.engine.parsing.StencilParser.RangeLiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.RefSelectorContext; import com.impossibl.stencil.engine.parsing.StencilParser.ReturnStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.SafeMemberSelectorContext; import com.impossibl.stencil.engine.parsing.StencilParser.SelectorContext; import com.impossibl.stencil.engine.parsing.StencilParser.SelectorExpressionContext; import com.impossibl.stencil.engine.parsing.StencilParser.SimpleNameContext; import com.impossibl.stencil.engine.parsing.StencilParser.StatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.StringLiteralContext; import com.impossibl.stencil.engine.parsing.StencilParser.SwitchOutputCaseContext; import com.impossibl.stencil.engine.parsing.StencilParser.SwitchOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.SwitchOutputDefaultCaseContext; import com.impossibl.stencil.engine.parsing.StencilParser.SwitchOutputValueCaseContext; import com.impossibl.stencil.engine.parsing.StencilParser.SwitchStatementCaseContext; import com.impossibl.stencil.engine.parsing.StencilParser.SwitchStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.SwitchStatementDefaultCaseContext; import com.impossibl.stencil.engine.parsing.StencilParser.SwitchStatementValueCaseContext; import com.impossibl.stencil.engine.parsing.StencilParser.TemplateContext; import com.impossibl.stencil.engine.parsing.StencilParser.TemplateImporterContext; import com.impossibl.stencil.engine.parsing.StencilParser.TernaryExpressionContext; import com.impossibl.stencil.engine.parsing.StencilParser.TextOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.TypeImporterContext; import com.impossibl.stencil.engine.parsing.StencilParser.UnaryExpressionContext; import com.impossibl.stencil.engine.parsing.StencilParser.UnnamedOutputBlockContext; import com.impossibl.stencil.engine.parsing.StencilParser.ValueSelectorContext; import com.impossibl.stencil.engine.parsing.StencilParser.VariableDeclContext; import com.impossibl.stencil.engine.parsing.StencilParser.VariableRefContext; import com.impossibl.stencil.engine.parsing.StencilParser.VariableRefExpressionContext; import com.impossibl.stencil.engine.parsing.StencilParser.WhileOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.WhileStatementContext; import com.impossibl.stencil.engine.parsing.StencilParser.WithOutputContext; import com.impossibl.stencil.engine.parsing.StencilParser.WithStatementContext; import com.impossibl.stencil.engine.parsing.StencilParserBaseVisitor; public class StencilInterpreter { private static final Logger logger = LogManager.getLogger(StencilInterpreter.class); /** * Environment */ private class Environment { private String path; private Writer out; public Environment(String path, Writer out) { this.path = path; this.out = out; } } /** * Scope holds values for current scope and has * a parent for managing a stack of scopes. * * @author kdubb * */ private static class Scope { private static class Null { @Override public String toString() { return "NULL"; } } private static final Null NULL = new Null(); protected StencilInterpreter interpreter; protected Environment declaringEnvironment; protected Scope parent; protected Map<String, Object> values = new HashMap<String, Object>(); public Scope(StencilInterpreter interpreter, Scope parent) { this.interpreter = interpreter; this.declaringEnvironment = interpreter.currentEnvironment; this.parent = parent; } protected Object unwrap(Object val) { return val != NULL ? val : null; } protected Object wrap(Object value) { return value != null ? value : NULL; } /** * Retrieves a variable from the given scope or * one of its parents * * @param name Name of variable to retrieve * @return Value of named variable * @throws UndefinedVariableException when the variable is undefined */ Object ref(String name, ParserRuleContext errCtx) { Object val = values.get(name); if(val == null) { if (parent == null) throw new UndefinedVariableException(name, interpreter.getLocation(errCtx)); return parent.ref(name, errCtx); } return unwrap(val); } /** * Retrieves a control variable from the current scope * or one of its parents * * @param name Name of control variable to retrieve * @return Value of named variable */ Object refControl(String name) { Object val = values.get(name); if(val == null) { if(parent == null) return null; return parent.refControl(name); } return unwrap(val); } /** * Declares a variable in this scope * * @param name Name of variable to declare * @param value Initial value of variable */ void declare(String name, Object value) { values.put(name, wrap(value)); } /** * Declares variables in this scope * @param map Name=>Value map of variables to declare */ void declare(Map<String, ?> map) { for(Map.Entry<String, ?> entry : map.entrySet()) values.put(entry.getKey(), wrap(entry.getValue())); } /** * Assigns a value to a variable in the current scope * @param name Name of variable to assign value to * @param value Value to assign to variable * @throws UndefinedVariableException when the variable is undefined */ void assign(String name, Object value, ParserRuleContext errCtx) { if (values.containsKey(name)) values.put(name, wrap(value)); else if (parent != null) parent.assign(name, value, errCtx); else throw new UndefinedVariableException(name, interpreter.getLocation(errCtx)); } /** * Assigns a control value to a variable in the current scope * @param name Name of variable to assign value to * @param value Value to assign to variable * @throws UndefinedVariableException when the variable is undefined */ void assignControl(String name, Object value) { if (values.containsKey(name)) values.put(name, wrap(value)); else if (parent != null) parent.assignControl(name, value); else throw new IllegalStateException("unknown control variable"); } @Override public String toString() { StringBuilder sb = new StringBuilder() .append(values.toString()) .append(" => "); if(parent != null) sb.append(parent); return sb.toString(); } } /** * Scope for callables that does not pass on * control variables * * @author kdubb * */ private static class CallableScope extends Scope { public CallableScope(StencilInterpreter interpreter, Scope parent) { super(interpreter, parent); } @Override public Object refControl(String name) { return unwrap(values.get(name)); } } /** * Root scope that searches all provided * globals scopes as a last resort * * @author kdubb * */ private static class RootScope extends Scope { Iterable<GlobalScope> globalScopes; public RootScope(StencilInterpreter interpreter, Iterable<GlobalScope> globalScopes) { super(interpreter, null); this.globalScopes = globalScopes; } @Override Object ref(String name, ParserRuleContext errCtx) { Object val = values.get(name); if (val == null) { return refGlobal(name); } return unwrap(values.get(name)); } private Object refGlobal(String name) { for(GlobalScope globalScope : globalScopes) { Object val = globalScope.get(name); if(val != GlobalScope.NOT_FOUND) return val; } return null; } } private static class WithScope extends Scope { Iterable<Object> objects; public WithScope(StencilInterpreter interpreter, Iterable<Object> objects) { super(interpreter, null); this.objects = objects; } @Override Object ref(String name, ParserRuleContext errCtx) { //Look for the variable in the local objects for(Object object : objects) { try { return PropertyUtils.getProperty(object, name); } catch (IllegalAccessException | InvocationTargetException e) { throw new ExecutionException(e, interpreter.getLocation(errCtx)); } catch (NoSuchMethodException e) { //Ignore... } } return super.ref(name, errCtx); } } /** * FunctionDefinition bound to the scope in which it was defined * * @author kdubb * */ private class BoundFunction { Scope owningScope; FunctionDefinitionContext source; /** * Constructs a bound function * @param source FunctionDefinition to bind * @param owningScope Scope to bind to */ public BoundFunction(FunctionDefinitionContext source, Scope owningScope) { this.owningScope = owningScope; this.source = source; } /** * Executes the referenced function in the bound scope * * @param parameters Name=>Value map of parameters for call * @return Result of function execution */ public Object call(Map<String, Object> parameters) { Scope prevScope = switchScope(owningScope); try { pushScope(new CallableScope(StencilInterpreter.this, currentScope)); try { //Declare return variable currentScope.declare(FUNC_RETURN_VAR, null); //Declare all parameters in the function's execution scope currentScope.declare(parameters); //Execute the function statements exec(source.blockStmt); //Return value (if any) return currentScope.refControl(FUNC_RETURN_VAR); } finally { popScope(); } } finally { switchScope(prevScope); } } @Override public String toString() { return "func " + name(source); } } /** * MacroDefinition bound to the scope in which it was defined * * @author kdubb * */ private class BoundMacro { Scope owningScope; MacroDefinitionContext source; /** * Constructs a bound macro * @param def MacroDefinition to bind * @param owningScope Scope to bind to */ public BoundMacro(MacroDefinitionContext def, Scope owningScope) { this.owningScope = owningScope; this.source = def; } @Override public String toString() { return "macro " + name(source); } } /** * Block bound to the macro and parameters/blocks passed to it * * @author kdubb * */ private class BoundBlock implements Block { BoundMacro source; Map<String, Object> parameters = new HashMap<String, Object>(); /** * Constructs a bound block * @param source */ public BoundBlock(BoundMacro source) { this.source = source; } /** * Binds more parameters to the block * @param parameters Parameters to bind to the block */ public void declareParams(Map<String, ?> params) { parameters.putAll(params); } /** * Binds more parameters to the block * @param parameters Parameters to bind to the block */ public void declareBlocks(Map<String, Object> blocks) { parameters.putAll(blocks); } /** * Evaluates the block in the scope in which its source macro was bound */ public void exec() { Scope prevScope = switchScope(source.owningScope); try { pushScope(); try { //Declare the parameters in the block's execution scope currentScope.declare(parameters); //Evaluate the block and return it's related text source.source.block.accept(visitor); } finally { popScope(); } } finally { switchScope(prevScope); } } @Override public boolean getHasOutput() { return true; } @Override public void write(Writer out) throws IOException { Writer prevOut = currentEnvironment.out; currentEnvironment.out = out; try { exec(); } finally { currentEnvironment.out = prevOut; } } @Override public String toString() { StringWriter out = new StringWriter(); try { write(out); } catch (IOException e) { throw new ExecutionException(e, null); } return out.toString(); } } /** * ParamOutputBlock bound to the scope in which it was defined and the * related replacement mode. * * @author kdubb * */ private class BoundParamOutputBlock implements Block { ParamOutputBlockMode mode; ParserRuleContext source; Scope owningScope; /** * Constructs a bound parameter block. The mode is taken from the source block * @param source ParamOutputBlock to bind * @param owningScope Scope to bind to */ public BoundParamOutputBlock(ParserRuleContext source, ParamOutputBlockMode mode, Scope owningScope) { this.source = source; this.mode = mode != null ? mode : ParamOutputBlockMode.Replace; this.owningScope = owningScope; } /** * Constructs an empty bound parameter block * @param mode OutputBlockMode to assign * @param owningScope Scope to bind to */ public BoundParamOutputBlock(ParamOutputBlockMode mode, Scope owningScope) { this.mode = mode != null ? mode : ParamOutputBlockMode.Replace; this.owningScope = owningScope; } /** * Evaluates block in the scope in which it was defined */ public void exec() { if (source == null) return; Scope prevScope = switchScope(owningScope); try { //Evaluate block source.accept(visitor); } finally { switchScope(prevScope); } } @Override public boolean getHasOutput() { return source != null; } @Override public void write(Writer out) throws IOException { Writer prevOut = currentEnvironment.out; currentEnvironment.out = out; try { exec(); } finally { currentEnvironment.out = prevOut; } } @Override public String toString() { StringWriter out = new StringWriter(); try { write(out); } catch (IOException e) { throw new ExecutionException(e, null); } return out.toString(); } } abstract class LValue { abstract Object get(ParserRuleContext errCtx); abstract void set(Object val, ParserRuleContext errCtx); LValue select(RefSelectorContext selector) { if(selector instanceof SafeMemberSelectorContext) { return new SafeSelectorLValue(get(selector), selector); } return new SelectorLValue(get(selector), selector); } } class ScopeLValue extends LValue { Scope scope; String name; public ScopeLValue(Scope scope, String name) { this.scope = scope; this.name = name; } @Override Object get(ParserRuleContext errCtx) { return scope.ref(name, errCtx); } @Override void set(Object val, ParserRuleContext errCtx) { scope.assign(name, val, errCtx); } } class SelectorLValue extends LValue { Object source; RefSelectorContext selector; SelectorLValue(Object source, RefSelectorContext selector) { this.source = source; this.selector = selector; } @Override Object get(ParserRuleContext errCtx) { return visitor.select(source, selector); } void nullAssignment() { logger.error("{}: attempt to assign to null lvalue", getLocation(selector)); } @Override void set(Object val, ParserRuleContext errCtx) { if(source == null) { nullAssignment(); return; } if(selector instanceof IndexSelectorContext) { IndexSelectorContext sel = (IndexSelectorContext)selector; setIndex(source, eval(sel.expr), val); } else if(selector instanceof MemberSelectorContext) { MemberSelectorContext sel = (MemberSelectorContext)selector; setMember(source, name(sel), false, val); } else if(selector instanceof SafeMemberSelectorContext) { SafeMemberSelectorContext sel = (SafeMemberSelectorContext)selector; setMember(source, name(sel), true, val); } else if(selector instanceof MemberIndexSelectorContext) { MemberIndexSelectorContext sel = (MemberIndexSelectorContext)selector; setMemberIndex(source, name(sel), eval(sel.expr), val); } } void setMemberIndex(Object source, String member, Object index, Object val) { if(index instanceof Number) { try { PropertyUtils.setIndexedProperty(source, member, ((Number) index).intValue(), val); } catch (IllegalAccessException | InvocationTargetException e) { throw new ExecutionException("invalid indexed member access", getLocation(selector)); } catch (NoSuchMethodException e) { //Ignore and keep trying... } } //Try as separate member -> index accesses Object memberSource = visitor.selectMember(source, member, false, selector); if(memberSource == null) { logger.error("{}: attempt to assign to null", getLocation(selector)); return; } setIndex(memberSource, index, val); } void setIndex(Object source, Object index, Object val) { if(index instanceof Number) { setIndex(source, ((Number) index).intValue(), val); } else { setMember(source, index.toString(), false, val); } } void setIndex(Object source, int index, Object val) { if(source.getClass().isArray()) { Array.set(source, index, val); } else if(source instanceof List<?>) { @SuppressWarnings("unchecked") List<Object> list = ((List<Object>)source); list.set(index, val); } else { throw new ExecutionException("Invalid index expression", getLocation(selector)); } } void setMember(Object source, String property, boolean safe, Object val) { try { PropertyUtils.setSimpleProperty(source, property, val); } catch (IllegalAccessException e) { throw new ExecutionException(e, getLocation(selector)); } catch (InvocationTargetException e) { throw new ExecutionException(e, getLocation(selector)); } catch (NoSuchMethodException e) { if(!safe) { logger.warn("{}: property '{}' does not exist", getLocation(selector), property); } } } } class SafeSelectorLValue extends SelectorLValue { SafeSelectorLValue(Object source, RefSelectorContext selector) { super(source, selector); } @Override void nullAssignment() { } } /** * Provides foreach iteration values * * @author kdubb * */ static public class ForeachIterator { Iterator<?> iter; Integer index; Object value; public ForeachIterator(Collection<?> source) { this.iter = source.iterator(); this.index = -1; } boolean next() { if(!iter.hasNext()) { index = null; value = null; return false; } value = iter.next(); index+=1; return true; } public boolean getHasNext() { return iter.hasNext(); } public boolean getOdd() { Integer count = getCount(); if(count == null) return false; return count % 2 == 1; } public boolean getEven() { Integer count = getCount(); if(count == null) return false; return count % 2 == 0; } public Integer getCount() { if(index == null) return null; return index+1; } public Integer getIndex() { return index; } public Object getValue() { return value; } } private static final String OUTPUT_LOOP_CONTROL_VAR = "##loop-control"; private static final String FUNC_RETURN_VAR = "##return"; private enum ControlStatements { Return, Break, Continue } private class Visitor extends StencilParserBaseVisitor<Object> { @Override public Object visitChildren(RuleNode node) { Object res = null; int n = node.getChildCount(); for (int i=0; i<n; i++) { res = node.getChild(i).accept(this); } return res; } @Override public Object visitTemplateImporter(TemplateImporterContext object) { TemplateImpl imported = load(value(object.stringLit), object); Environment prevEnv = switchEnvironment(new Environment(imported.getPath(), new NullWriter())); pushScope(); Scope importedScope; try { imported.getContext().accept(this); } finally { importedScope = popScope(); switchEnvironment(prevEnv); } //Import defintions if(object.id == null) { currentScope.values.putAll(importedScope.values); } else { currentScope.declare(object.id.getText(), importedScope.values); } return null; } @Override public Object visitTypeImporter(TypeImporterContext object) { String typeFullName = value(object.qName); String localName = value(object.id); if(localName == null) { localName = Iterables.getLast(Splitter.on('.').split(typeFullName), ""); } Class<?> type = loadClass(typeFullName); if(type == null) { throw new UndefinedTypeException(typeFullName, getLocation(object)); } currentScope.declare(localName, type); return null; } @Override public Object visitMacroDefinition(MacroDefinitionContext object) { BoundMacro boundMacro = new BoundMacro(object, currentScope); currentScope.declare(value(object.id), boundMacro); return null; } @Override public Object visitFunctionDefinition(FunctionDefinitionContext object) { BoundFunction boundFunction = new BoundFunction(object, currentScope); currentScope.declare(value(object.id), boundFunction); return null; } @Override public Object visitExportDefinition(ExportDefinitionContext object) { for(VariableDeclContext varDecl : object.vars) { currentScope.declare(value(varDecl.id), eval(varDecl.expr)); } return null; } @Override public Object visitOutputBlock(OutputBlockContext object) { for (OutputContext content : object.outputs) { content.accept(this); Object outputLoopControl = currentScope.refControl(OUTPUT_LOOP_CONTROL_VAR); if(outputLoopControl == ControlStatements.Break || outputLoopControl == ControlStatements.Continue) { break; } } return null; } @Override public Object visitOutput(OutputContext ctx) { return ctx.getChild(0).accept(visitor); } @Override public Object visitDynamicOutput(DynamicOutputContext ctx) { return ctx.getChild(1).accept(visitor); } @Override public Object visitTextOutput(TextOutputContext object) { output(object.getText()); return null; } @Override public Object visitIncludeOutput(IncludeOutputContext object) { TemplateImpl included = load(value(object.stringLit), object); Map<String, Object> params; Map<String, Object> blocks; HeaderContext hdr = included.getContext().hdr; if(hdr.hdrSig != null) { params = bindParams(hdr.hdrSig.callSig, object.call); blocks = bindBlocks(hdr.hdrSig.prepSig, object.prep); } else { params = Collections.emptyMap(); blocks = Collections.emptyMap(); } Environment prevEnv = switchEnvironment(new Environment(included.getPath(), currentEnvironment.out)); pushScope(); try { currentScope.declare(params); currentScope.declare(blocks); included.getContext().accept(visitor); } finally { popScope(); switchEnvironment(prevEnv); } return null; } @Override public Object visitExpressionOutput(ExpressionOutputContext object) { if(logger.isTraceEnabled()) { logger.trace("Evaluating expression: {}", object.getText()); } Object value = eval(object.expr); if (value == null) { logger.info("Expression evaluates to null: {}", value(object)); } else if (value instanceof BoundMacro) { // //Output a bound macro definition // BoundBlock boundBlock = new BoundBlock((BoundMacro) value); Map<String, Object> boundBlocks = bindBlocks(boundBlock.source.source.prepSig, object.prep); boundBlock.declareBlocks(boundBlocks); boundBlock.exec(); } else if (value instanceof BoundBlock) { // //Output a bound block // BoundBlock boundBlock = (BoundBlock) value; Map<String, Object> boundBlocks = bindBlocks(boundBlock.source.source.prepSig, object.prep); boundBlock.declareBlocks(boundBlocks); boundBlock.exec(); } else if (value instanceof BoundParamOutputBlock) { // //Output a bound parameter block // BoundParamOutputBlock boundParamBlock = (BoundParamOutputBlock) value; UnnamedOutputBlockContext defaultBlock = object.prep != null ? object.prep.unnamedOutputBlock() : null; switch (boundParamBlock.mode) { case Before: boundParamBlock.exec(); if (defaultBlock != null) defaultBlock.accept(this); break; case After: if (defaultBlock != null) defaultBlock.accept(this); boundParamBlock.exec(); break; case Replace: boundParamBlock.exec(); break; } } else if(value instanceof Preparable) { Preparable function = (Preparable) value; PrepareSignatureContext sig; try { sig = extensionPreparableSignatureCache.get(function); } catch (java.util.concurrent.ExecutionException e) { throw new RuntimeException(e); } Map<String, Object> params = bindBlocks(sig, object.prep); try { output(function.prepare(params)); } catch (Throwable e) { throw new InvocationException("error invoking extension function", e, getLocation(object)); } } else { //Ensure a preparation isn't incorrectly attempted if (object.prep != null && (object.prep.namedBlocks.isEmpty() == false || object.prep.unnamedBlock != null)) { throw new ExecutionException("Illegal block call", getLocation(object)); } output(value); } return null; } @Override public Object visitDeclarationOutput(DeclarationOutputContext object) { for (VariableDeclContext decl : object.vars) { String name = name(decl); Object value = eval(decl.expr); currentScope.declare(name, value); } return null; } @Override public Object visitAssignOutput(AssignOutputContext object) { for(AssignmentContext ass : object.assignments) { LValue lvalue = eval(ass.lValRef); Object val = eval(ass.expr); assign(lvalue, value(ass.assignmntOper), val, ass); } return null; } @Override public Object visitIfOutput(IfOutputContext object) { boolean cond = evalBoolean(object.expr); OutputBlockContext out = cond ? object.thenBlock : object.elseBlock; if (out != null) out.accept(this); return null; } @Override public Object visitForeachOutput(ForeachOutputContext object) { Collection<?> collection = evalCollection(object.expr); pushScope(); currentScope.declare(OUTPUT_LOOP_CONTROL_VAR, null); try { if (collection != null && collection.isEmpty() == false) { ForeachIterator iter = new ForeachIterator(collection); if(object.iterator != null) { currentScope.declare(name(object.iterator), iter); } while(iter.next() && currentScope.refControl(OUTPUT_LOOP_CONTROL_VAR) != ControlStatements.Break) { currentScope.assignControl(OUTPUT_LOOP_CONTROL_VAR, null); currentScope.declare(name(object.value), iter.getValue()); object.iterBlock.accept(this); } } else { if (object.elseBlock != null) object.elseBlock.accept(this); } } finally { popScope(); } return null; } @Override public Object visitWhileOutput(WhileOutputContext object) { ExpressionContext condExpr = object.expr; pushScope(); currentScope.declare(OUTPUT_LOOP_CONTROL_VAR, null); try { while (evalBoolean(condExpr) && currentScope.refControl(OUTPUT_LOOP_CONTROL_VAR) != ControlStatements.Break) { currentScope.assignControl(OUTPUT_LOOP_CONTROL_VAR, null); object.block.accept(this); } } finally { popScope(); } return null; } @Override public Object visitBreakOutput(BreakOutputContext object) { currentScope.assignControl(OUTPUT_LOOP_CONTROL_VAR, ControlStatements.Break); return null; } @Override public Object visitContinueOutput(ContinueOutputContext object) { currentScope.assignControl(OUTPUT_LOOP_CONTROL_VAR, ControlStatements.Continue); return null; } @Override public Object visitSwitchOutput(SwitchOutputContext object) { Object switchValue = eval(object.expr); boolean found = false; SwitchOutputDefaultCaseContext defaultCase=null; for (SwitchOutputCaseContext switchCase : object.cases) { if(switchCase instanceof SwitchOutputValueCaseContext) { SwitchOutputValueCaseContext valueCase = (SwitchOutputValueCaseContext) switchCase; Object caseValue = eval(valueCase.expr); if (operands.equals(switchValue, caseValue)) { valueCase.block.accept(this); found = true; } } else if(switchCase instanceof SwitchOutputDefaultCaseContext){ defaultCase = (SwitchOutputDefaultCaseContext) switchCase; } } if (!found && defaultCase != null) { defaultCase.block.accept(this); } return null; } @Override public Object visitWithOutput(WithOutputContext object) { //Declare all variables List<Object> vars = eval(object.expr); pushScope(new WithScope(StencilInterpreter.this, vars)); try { object.block.accept(this); } finally { popScope(); } return null; } @Override public Object visitBlockStatement(BlockStatementContext object) { pushScope(); try { for (StatementContext statement : object.stmts) { Object res = statement.accept(visitor); if (res != null) return res; } return null; } finally { popScope(); } } @Override public Object visitExpressionStatement(ExpressionStatementContext object) { eval(object.expr); return null; } @Override public Object visitDeclarationStatement(DeclarationStatementContext object) { for (VariableDeclContext decl : object.vars) { String name = name(decl); Object value = eval(decl.expr); currentScope.declare(name, value); } return null; } @Override public Object visitAssignmentStatement(AssignmentStatementContext object) { LValue lvalue = eval(object.assignmnt.lValRef); Object val = eval(object.assignmnt.expr); assign(lvalue, value(object.assignmnt.assignmntOper), val, object); return null; } @Override public Object visitReturnStatement(ReturnStatementContext object) { Object val = eval(object.expr); currentScope.assignControl(FUNC_RETURN_VAR, val); return ControlStatements.Return; } @Override public Object visitBreakStatement(BreakStatementContext ctx) { return ControlStatements.Break; } @Override public Object visitContinueStatement(ContinueStatementContext ctx) { return ControlStatements.Continue; } @Override public Object visitIfStatement(IfStatementContext object) { boolean condition = evalBoolean(object.expr); if (condition) { return exec(object.thenStmt); } else { return exec(object.elseStmt); } } @Override public Object visitForeachStatement(ForeachStatementContext object) { Collection<?> collection = evalCollection(object.expr); pushScope(); try { Object res = null; if (collection != null && collection.isEmpty() == false) { ForeachIterator iter = new ForeachIterator(collection); if(object.iterator != null) { currentScope.declare(name(object.iterator), iter); } while(iter.next()) { currentScope.declare(name(object.value), iter.getValue()); Object sres = exec(object.iterStmt); if(sres == ControlStatements.Break) { break; } else if(sres == ControlStatements.Continue) { continue; } else if(sres == ControlStatements.Return) { res = sres; break; } } } else { res = exec(object.elseStmt); } return res; } finally { popScope(); } } @Override public Object visitWhileStatement(WhileStatementContext object) { ExpressionContext condExpr = object.expr; while (evalBoolean(condExpr)) { Object res = exec(object.stmt); if(res == ControlStatements.Break) { break; } else if(res == ControlStatements.Continue) { continue; } else if (res == ControlStatements.Return) { return res; } } return null; } @Override public Object visitSwitchStatement(SwitchStatementContext object) { Object switchValue = eval(object.expr); boolean found = false; SwitchStatementDefaultCaseContext defaultCase = null; for(SwitchStatementCaseContext switchCase : object.cases) { if(switchCase instanceof SwitchStatementValueCaseContext) { SwitchStatementValueCaseContext valueCase = (SwitchStatementValueCaseContext) switchCase; Object caseValue = eval(valueCase.expr); if(operands.equals(switchValue,caseValue)) { exec(valueCase.stmt); found = true; } } else if(switchCase instanceof SwitchStatementDefaultCaseContext) { defaultCase = (SwitchStatementDefaultCaseContext) switchCase; } } if (!found && defaultCase != null) { exec(defaultCase.stmt); } return null; } @Override public Object visitWithStatement(WithStatementContext object) { //Declare all variables List<Object> vars = eval(object.expr); pushScope(new WithScope(StencilInterpreter.this, vars)); try { exec(object.stmt); } finally { popScope(); } return null; } @Override public LValue visitLValueRef(LValueRefContext object) { LValue val = new ScopeLValue(currentScope, name(object)); for(RefSelectorContext sel : object.refSel) { val = val.select(sel); } return val; } @Override public Object visitVariableRef(VariableRefContext object) { return currentScope.ref(name(object), object); } @Override public Object visitSimpleName(SimpleNameContext ctx) { return ctx.getChild(0); } @Override public Object visitLiteral(LiteralContext ctx) { return ctx.getChild(0).accept(visitor); } @Override public Object visitNullLiteral(NullLiteralContext object) { return null; } @Override public Object visitBooleanLiteral(BooleanLiteralContext object) { return value(object); } @Override public Object visitNumberLiteral(NumberLiteralContext ctx) { return ctx.getChild(0).accept(visitor); } @Override public Object visitIntegerLiteral(IntegerLiteralContext object) { return value(object); } @Override public Object visitFloatingLiteral(FloatingLiteralContext object) { return value(object); } @Override public Object visitStringLiteral(StringLiteralContext object) { return value(object); } @Override public Object visitListLiteral(ListLiteralContext object) { List<Object> list = new ArrayList<Object>(); for (ExpressionContext itemExpr : object.expr) { list.add(eval(itemExpr)); } return list; } @Override public Object visitMapLiteral(MapLiteralContext object) { Map<String, Object> map = new HashMap<String, Object>(); for (NamedValueContext namedValue : object.namedValues) { Object value = eval(namedValue.expr); map.put(name(namedValue), value); } return map; } @Override public Object visitRangeLiteral(RangeLiteralContext object) { int low = evalInteger(object.from); int hi = evalInteger(object.to); Set<?> range = ContiguousSet.create(Range.closedOpen(low, hi), DiscreteDomain.integers()); return range; } @Override public Object visitTernaryExpression(TernaryExpressionContext object) { Object test = eval(object.test); if(operands.toBoolean(test)) { if(object.trueExpr == null) return test; else return eval(object.trueExpr); } else { return eval(object.falseExpr); } } @Override public Object visitInstanceExpression(InstanceExpressionContext ctx) { String typeFullName = value(ctx.qName); Class<?> type = loadClass(typeFullName); if(type == null) { Object val = currentScope.ref(typeFullName, ctx.qName); if(val instanceof Class<?>) { type = (Class<?>) val; } else { throw new UndefinedTypeException(typeFullName, getLocation(ctx)); } } Object value = eval(ctx.expr); return type.isInstance(value); } @Override public Object visitParenExpression(ParenExpressionContext ctx) { return eval(ctx.expr); } @Override public Object visitBinaryExpression(BinaryExpressionContext object) { ExpressionContext left = object.leftExpr, right = object.rightExpr; String oper = value(object.operator); if (oper.equals("==")) { return operands.equals(eval(left), eval(right)); } else if (oper.equals("===")) { return eval(left) == eval(right); } else if (oper.equals("!==")) { return !(eval(left) == eval(right)); } else if (oper.equals("!=")) { return !operands.equals(eval(left), eval(right)); } else if (oper.equals(">")) { return operands.greaterThan(eval(left), eval(right)); } else if (oper.equals(">=")) { return operands.greaterThanOrEqual(eval(left), eval(right)); } else if (oper.equals("<")) { return operands.lessThan(eval(left), eval(right)); } else if (oper.equals("<=")) { return operands.lessThanOrEqual(eval(left), eval(right)); } else if (oper.equals("+")) { return operands.add(eval(left), eval(right), left); } else if (oper.equals("-")) { return operands.subtract(eval(left), eval(right)); } else if (oper.equals("*")) { return operands.multiply(eval(left), eval(right)); } else if (oper.equals("/")) { return operands.divide(eval(left), eval(right)); } else if (oper.equals("%")) { return operands.mod(eval(left), eval(right)); } else if (oper.equals(">>")) { return operands.rightShift(eval(left), eval(right)); } else if (oper.equals("<<")) { return operands.leftShift(eval(left), eval(right)); } else if (oper.equals("&&")) { return evalBoolean(left) && evalBoolean(right); } else if (oper.equals("||")) { return evalBoolean(left) || evalBoolean(right); } else if (oper.equals("&")) { return operands.bitwiseAnd(eval(left), eval(right)); } else if (oper.equals("|")) { return operands.bitwiseOr(eval(left), eval(right)); } else if (oper.equals("^")) { return operands.bitwiseXor(eval(left), eval(right)); } throw new ExecutionException("invalid expression operator", getLocation(object)); } @Override public Object visitUnaryExpression(UnaryExpressionContext object) { Object val = eval(object.expr); String oper = value(object.operator); if (oper.equals("++")) { return operands.add(val, 1, object); } else if (oper.equals("--")) { return operands.subtract(val, 1); } else if (oper.equals("~")) { return operands.bitwiseComplement(val); } else if (oper.equals("!")) { return !operands.toBoolean(val); } else if (oper.equals("+")) { return val; } else if (oper.equals("-")) { return operands.negate(val); } throw new ExecutionException("invalid prefix expression",getLocation(object)); } @Override public Object visitVariableRefExpression(VariableRefExpressionContext ctx) { return eval(ctx.varRef); } @Override public Object visitLiteralExpression(LiteralExpressionContext ctx) { return ctx.lit.accept(visitor); } @Override public Object visitSelectorExpression(SelectorExpressionContext object) { Object res = eval(object.expr); for(SelectorContext sel : object.sels) { res = select(res, sel); if(res == null) break; } return res; } Object select(Object source, SelectorContext sel) { if(sel.valSel != null) { return select(source, sel.valSel); } else if(sel.refSel != null) { return select(source, sel.refSel); } return source; } Object select(Object source, ValueSelectorContext sel) { if(sel instanceof CallSelectorContext) { return select(source, (CallSelectorContext)sel); } if(sel instanceof MethodCallSelectorContext) { return select(source, (MethodCallSelectorContext)sel); } throw new ExecutionException("unknown selector", getLocation(sel)); } Object select(Object source, RefSelectorContext sel) { if(sel instanceof MemberIndexSelectorContext) { return select(source, (MemberIndexSelectorContext)sel); } if(sel instanceof IndexSelectorContext) { return select(source, (IndexSelectorContext)sel); } if(sel instanceof MemberSelectorContext) { return select(source, (MemberSelectorContext)sel); } if(sel instanceof SafeMemberSelectorContext) { return select(source, (SafeMemberSelectorContext)sel); } throw new ExecutionException("unknown selector", getLocation(sel)); } Object select(Object source, CallSelectorContext sel) { if(logger.isTraceEnabled()) { logger.trace("Evaluating call selector: {}", sel.getText()); } return selectCall(source, sel.call, sel); } Object select(Object source, MethodCallSelectorContext sel) { if(logger.isTraceEnabled()) { logger.trace("Evaluating method call selector: {}", sel.getText()); } if (source == null) { logger.error("{}: attempt to call method on null", getLocation(sel)); return null; } else { String name = name(sel); List<Object> params = sel.call.posParams != null ? eval(sel.call.posParams.exprs) : Collections.emptyList(); try { if(source instanceof Class<?>) { return MethodUtils.invokeStaticMethod((Class<?>) source, name, params.toArray()); } else { return MethodUtils.invokeMethod(source, name, params.toArray()); } } catch (IllegalAccessException | InvocationTargetException e) { throw new ExecutionException("error executing call", e, getLocation(sel)); } catch (NoSuchMethodException e) { Method extensionMethod = getExtensionMethod(source.getClass(), name); if(extensionMethod != null) { params.add(0, source); try { return extensionMethod.invoke(null, params.toArray()); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e1) { throw new ExecutionException("error executing call", getLocation(sel)); } } else { source = selectMember(source, name, true, sel); if(source != null) { return selectCall(source, sel.call, sel); } return null; } } } } Object select(Object source, MemberIndexSelectorContext sel) { if(logger.isTraceEnabled()) { logger.trace("Evaluating member index selector: {}", sel.getText()); } String member = name(sel); Object index = eval(sel.expr); return selectMemberIndex(source, member, index, sel); } Object select(Object source, IndexSelectorContext sel) { if(logger.isTraceEnabled()) { logger.trace("Evaluating index selector: {}", sel.getText()); } Object selectorValue = eval(sel.expr); return selectIndex(source, selectorValue, sel); } Object select(Object source, MemberSelectorContext sel) { if(logger.isTraceEnabled()) { logger.trace("Evaluating member selector: {}", sel.getText()); } if(source == null) { logger.warn("{}: unsafe null navigation", getLocation(sel)); return null; } return selectMember(source, name(sel), false, sel); } Object select(Object source, SafeMemberSelectorContext sel) { if(logger.isTraceEnabled()) { logger.trace("Evaluating safe member selector: {}", sel.getText()); } if(source == null) { return null; } return selectMember(source, name(sel), true, sel); } Object selectCall(Object source, CallableInvocationContext inv, ParserRuleContext loc) { if (source == null) { logger.error("Attempt to call null " + getLocation(loc)); return null; } else if (source instanceof BoundMacro) { BoundMacro boundMacro = (BoundMacro) source; BoundBlock boundBlock = new BoundBlock(boundMacro); Map<String, Object> params = bindParams(boundMacro.source.callSig, inv); boundBlock.declareParams(params); return boundBlock; } else if (source instanceof BoundFunction) { BoundFunction boundFunction = (BoundFunction) source; Map<String, Object> params = bindParams(boundFunction.source.callSig, inv); return boundFunction.call(params); } else if(source instanceof Callable) { Callable function = (Callable) source; CallableSignatureContext sig; try { sig = extensionCallableSignatureCache.get(function); } catch (java.util.concurrent.ExecutionException e) { throw new RuntimeException(e); } Map<String,Object> params = bindParams(sig, inv); try { return function.call(params); } catch (Throwable e) { throw new InvocationException("error invoking extension function", e, getLocation(loc)); } } else if(source instanceof Class<?>) { Class<?> type = (Class<?>) source; if(inv.namedParams != null) { throw new InvocationException("error invoking constructor: named parameters not allowed for constructors", getLocation(inv)); } List<Object> paramValues; if(inv.posParams != null) { paramValues = eval(inv.posParams.exprs); } else { paramValues = Collections.emptyList(); } try { return ConstructorUtils.invokeConstructor(type, paramValues.toArray()); } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) { throw new ExecutionException("error invoking constructor: ", getLocation(inv)); } catch (NoSuchMethodException e) { return null; } } throw new ExecutionException("invalid call expression", getLocation(inv)); } Object selectMemberIndex(Object source, String member, Object index, ParserRuleContext loc) { if(source == null) { logger.error("{}: attempt to index null", getLocation(loc)); return null; } if(index instanceof Number) { try { return PropertyUtils.getIndexedProperty(source, member, ((Number) index).intValue()); } catch (IllegalAccessException | InvocationTargetException e) { throw new ExecutionException("invalid indexed member access", getLocation(loc)); } catch (NoSuchMethodException e) { //Ignore and keep trying... } } //Try as separate member -> index accesses Object memberSource = selectMember(source, member, false, loc); if(memberSource == null) { logger.error("{}: unsafe null navigation", getLocation(loc)); return null; } return selectIndex(memberSource, index, loc); } Object selectIndex(Object source, Object index, ParserRuleContext loc) { if(source == null) { logger.error("{}: attempt to index null", getLocation(loc)); return null; } if(index instanceof Number) { return selectIndex(source, ((Number) index).intValue(), loc); } else { return selectMember(source, index.toString(), false, loc); } } Object selectIndex(Object source, int index, ParserRuleContext loc) { if(source == null) { logger.error("{}: attempt to index null", getLocation(loc)); return null; } if(source.getClass().isArray()) { return Array.get(source, index); } else if(source instanceof List<?>) { List<?> list = ((List<?>)source); return list.get(index); } else if(source instanceof Map<?,?>) { Map<?, ?> map = ((Map<?, ?>)source); return map.get(index); } else { throw new ExecutionException("invalid index expression", getLocation(loc)); } } Object selectMember(Object source, String property, boolean safe, ParserRuleContext loc) { if(source == null) { logger.error("{}: attempt to access null", getLocation(loc)); return null; } try { return PropertyUtils.getProperty(source, property); } catch (IllegalAccessException | InvocationTargetException e) { throw new ExecutionException(e, getLocation(loc)); } catch (NoSuchMethodException e) { if(!safe) { logger.error("{}: property '{}' does not exist", getLocation(loc), property); } } return null; } void assign(LValue lvalue, String oper, Object right, ParserRuleContext errCtx) { if (oper.equals("=")) { lvalue.set(right, errCtx); } else if (oper.equals("+=")) { lvalue.set(operands.add(lvalue.get(errCtx), right, errCtx), errCtx); } else if (oper.equals("-=")) { lvalue.set(operands.subtract(lvalue.get(errCtx), right), errCtx); } else if (oper.equals("*=")) { lvalue.set(operands.multiply(lvalue.get(errCtx), right), errCtx); } else if (oper.equals("/=")) { lvalue.set(operands.divide(lvalue.get(errCtx), right), errCtx); } else if (oper.equals("%=")) { lvalue.set(operands.mod(lvalue.get(errCtx), right), errCtx); } else if (oper.equals("&=")) { lvalue.set(operands.bitwiseAnd(lvalue.get(errCtx), right), errCtx); } else if (oper.equals("|=")) { lvalue.set(operands.bitwiseOr(lvalue.get(errCtx), right), errCtx); } else if (oper.equals("^=")) { lvalue.set(operands.bitwiseXor(lvalue.get(errCtx), right), errCtx); } else if (oper.equals("<<=")) { lvalue.set(operands.leftShift(lvalue.get(errCtx), right), errCtx); } else if (oper.equals(">>=")) { lvalue.set(operands.rightShift(lvalue.get(errCtx), right), errCtx); } else { throw new ExecutionException("invalid assignment operator", getLocation(errCtx)); } } } private StencilEngine engine; private Visitor visitor = new Visitor(); private Environment currentEnvironment; private Scope currentScope; private StencilOperands operands = new StencilOperands(this); private LoadingCache<Callable,CallableSignatureContext> extensionCallableSignatureCache = CacheBuilder.newBuilder().build(new CacheLoader<Callable,CallableSignatureContext>() { @Override public CallableSignatureContext load(Callable key) throws Exception { String[] paramNames = key.getParameterNames(); CallableSignatureContext sig = new CallableSignatureContext(null, -1); sig.paramDecls = new ArrayList<>(); for(String paramName : paramNames) { if (paramName.equals(ALL_PARAM_NAME)) { sig.paramDecls.add(Contexts.createAllParameterDecl(sig, ALL_PARAM_NAME)); } else { sig.paramDecls.add(Contexts.createParameterDecl(sig, paramName)); } } return sig; } }); private LoadingCache<Preparable,PrepareSignatureContext> extensionPreparableSignatureCache = CacheBuilder.newBuilder().build(new CacheLoader<Preparable,PrepareSignatureContext>() { @Override public PrepareSignatureContext load(Preparable key) throws Exception { String[] blockNames = key.getBlockNames(); PrepareSignatureContext sig = new PrepareSignatureContext(null, -1); sig.blockDecls = new ArrayList<>(); for(String blockName : blockNames) { if (blockName.equals(ALL_BLOCK_NAME)) { sig.blockDecls.add(Contexts.createAllBlockDecl(sig, blockName)); } else if (blockName.equals(UNNAMED_BLOCK_NAME)) { sig.blockDecls.add(Contexts.createUnnamedBlockDecl(sig, blockName)); } else { sig.blockDecls.add(Contexts.createBlockDecl(sig, blockName)); } } return sig; } }); public StencilInterpreter(StencilEngine engine, Iterable<GlobalScope> globalScopes) { this.engine = engine; this.currentScope = new RootScope(StencilInterpreter.this, globalScopes); } public StencilInterpreter declare(Map<String,Object> parameters) { currentScope.declare(parameters); return this; } /** * Processes a template into its text representation */ public void process(TemplateImpl template, Writer out) throws IOException { Environment env = new Environment(template.getPath(), out); currentScope.declaringEnvironment = env; Environment prevEnv = switchEnvironment(env); TemplateContext tmpl = template.getContext(); HeaderContext hdr = tmpl.hdr; if (hdr != null && hdr.hdrSig != null && hdr.hdrSig.callSig != null && hdr.hdrSig.callSig.paramDecls != null) { for (ParameterDeclContext paramDecl : hdr.hdrSig.callSig.paramDecls) { String paramId = paramDecl.id.getText(); if (!currentScope.values.containsKey(paramId)) { currentScope.declare(paramId, eval(paramDecl.expr)); } } } try { tmpl.accept(visitor); } finally { switchEnvironment(prevEnv); } } /** * Loads a template from the supplied template loader. * */ private TemplateImpl load(String path, ParserRuleContext errCtx) { path = Paths.resolvePath(currentEnvironment.path, path); try { return (TemplateImpl) engine.load(path); } catch (IOException | ParseException e) { throw new ExecutionException("error importing " + path, e, getLocation(errCtx)); } } /** * */ private Class<?> loadClass(String className) { try { return StencilEngine.class.getClassLoader().loadClass(className); } catch (ClassNotFoundException e) { return null; } } /** * Switch to a new environment * * @param env Environment to switch to * @returns Previous environment */ private Environment switchEnvironment(Environment env) { Environment prev = currentEnvironment; currentEnvironment = env; return prev; } /** * Switch to a completely new scope stack * * @param scope Top of scope stack to switch to * @return Top of previous scope stack */ private Scope switchScope(Scope scope) { Scope prev = currentScope; currentScope = scope; return prev; } /** * Pushes a new scope onto the scope stack * */ private void pushScope() { pushScope(new Scope(this, currentScope)); } /** * Pushes a scope onto the scope stack * * @param scope Scope to be pushed onto the stack */ private void pushScope(Scope scope) { scope.parent = currentScope; currentScope = scope; } /** * Pops scope from the scope stack returning the current previously current * scope * * @return Scope prior to popping */ private Scope popScope() { Scope prevScope = currentScope; currentScope = currentScope.parent; return prevScope; } /** * Bind parameter values to names based on how the call was invoked (named or * positional) and parameters names from the call signature. * * @param sig Signature of call * @param inv Invocation of call * @return Map of String => Object representing call parameters */ private Map<String, Object> bindParams(CallableSignatureContext sig, CallableInvocationContext inv) { if(inv == null) { return Collections.emptyMap(); } Map<String, Object> vals = new HashMap<>(); if (inv.posParams != null) { // // Ensure legality of call // // Can only use positional or named; not both if (inv.namedParams != null) { throw new InvocationException("only positional or named parameters are allowed", getLocation(inv)); } // // Match up parameters // ListIterator<ParameterDeclContext> paramDeclIter = sig.paramDecls.listIterator(); ListIterator<ExpressionContext> paramExprIter = inv.posParams.exprs.listIterator(); ParameterDeclContext allDecl = null; while (paramDeclIter.hasNext()) { ParameterDeclContext paramDecl = paramDeclIter.next(); if (paramDecl.flag != null && paramDecl.flag.getText().equals("*")) { if (allDecl != null) { throw new ExecutionException("only a single parameter can be marked with '*'", getLocation(paramDecl)); } allDecl = paramDecl; continue; } String paramName = name(paramDecl); // Find expression (or use default) ExpressionContext paramExpr = paramExprIter.hasNext() ? paramExprIter.next() : paramDecl.expr; // Evaluate expression -> value Object paramVal = eval(paramExpr); vals.put(paramName, paramVal); } // // Add rest of parameters (if requested) // if (allDecl != null) { Map<String, Object> otherParams = new LinkedHashMap<>(); while (paramExprIter.hasNext()) { int paramIdx = paramExprIter.nextIndex(); ExpressionContext paramExpr = paramExprIter.next(); Object paramVal = eval(paramExpr); otherParams.put(Integer.toString(paramIdx), paramVal); } vals.put(allDecl.id.getText(), otherParams); } } else if (inv.namedParams != null) { // // Ensure legality of call // // Can only use positional or named; not both if (inv.posParams != null) { throw new InvocationException("only positional or named parameters are allowed", getLocation(inv)); } // // Load parameters // List<NamedValueContext> namedParameters = new ArrayList<>(inv.namedParams.namedValues); Iterator<ParameterDeclContext> paramDeclIter = sig.paramDecls.iterator(); ParameterDeclContext allDecl = null; while (paramDeclIter.hasNext()) { ParameterDeclContext paramDecl = paramDeclIter.next(); if (paramDecl.flag != null && paramDecl.flag.getText().equals("*")) { allDecl = paramDecl; continue; } String paramName = name(paramDecl); // Find expression ExpressionContext paramExpr = findAndRemoveValue(namedParameters, paramName); // Assign default (is needed) paramExpr = paramExpr != null ? paramExpr : paramDecl.expr; // Evaluate expression -> value Object paramVal = eval(paramExpr); vals.put(paramName, paramVal); } // // Add rest of parameters (if requested) // if (allDecl != null) { Map<String, Object> otherParams = new LinkedHashMap<>(); for (NamedValueContext namedValue : namedParameters) { String paramName = namedValue.name.getText(); Object paramVal = eval(namedValue.expr); otherParams.put(paramName, paramVal); } vals.put(allDecl.id.getText(), otherParams); } } return vals; } /** * Bind block values to names based on prepare signature * * @param sig Signature of output block preparation * @param inv Invocation of output block * @return Map of Name => BoundParamBlocks representing Macro blocks */ private Map<String, Object> bindBlocks(PrepareSignatureContext sig, PrepareInvocationContext inv) { if(inv == null) { return Collections.emptyMap(); } BlockDeclContext allDecl = null; BlockDeclContext unnamedDecl = null; List<NamedOutputBlockContext> namedBlocks = new ArrayList<>(inv.namedBlocks); Map<String, Object> blocks = new HashMap<>(); for (BlockDeclContext blockDecl : sig.blockDecls) { if (blockDecl.flag != null) { if (blockDecl.flag.getText().equals("*")) { if (allDecl != null) { throw new ExecutionException("only a single parameter can be marked with '*'", getLocation(blockDecl)); } allDecl = blockDecl; } else if (blockDecl.flag.getText().equals("+")) { if (unnamedDecl != null) { throw new ExecutionException("only a single parameter can be marked with '+'", getLocation(blockDecl)); } unnamedDecl = blockDecl; } else { throw new ExecutionException("unknown block declaration flag", getLocation(blockDecl)); } continue; } //Find the block ParserRuleContext paramBlock = findAndRemoveBlock(namedBlocks, name(blockDecl)); //Bind the block BoundParamOutputBlock boundBlock = bindBlock(paramBlock); blocks.put(name(blockDecl), boundBlock); } // // Bind unnamed block (if requested) // if (unnamedDecl != null) { UnnamedOutputBlockContext unnamedBlock = inv.unnamedBlock; BoundParamOutputBlock boundUnnamedBlock = bindBlock(unnamedBlock); blocks.put(unnamedDecl.id.getText(), boundUnnamedBlock); } // // Bind rest of blocks (if requested) // if (allDecl != null) { Map<String, Block> otherBlocks = new HashMap<>(); // Add unnamed block if it wasn't bound explicitly if (inv.unnamedBlock != null && unnamedDecl == null) { UnnamedOutputBlockContext unnamedBlock = inv.unnamedBlock; BoundParamOutputBlock boundUnnamedBlock = new BoundParamOutputBlock(unnamedBlock, mode(unnamedBlock), currentScope); otherBlocks.put("", boundUnnamedBlock); } // Add all other unbound blocks for (NamedOutputBlockContext namedBlock : namedBlocks) { String blockName = nullToEmpty(name(namedBlock)); BoundParamOutputBlock boundNamedBlock = new BoundParamOutputBlock(namedBlock, mode(namedBlock), currentScope); otherBlocks.put(blockName, boundNamedBlock); } blocks.put(allDecl.id.getText(), otherBlocks); } return blocks; } private BoundParamOutputBlock bindBlock(ParserRuleContext paramBlock) { BoundParamOutputBlock boundBlock; if (paramBlock != null) { //Simply bind with found block boundBlock = new BoundParamOutputBlock(paramBlock, mode(paramBlock), currentScope); } else { //Couldn't find a block with the declared name so we bind a //"null" block with mode "AFTER" to ensure default is used boundBlock = new BoundParamOutputBlock(ParamOutputBlockMode.After, currentScope); } return boundBlock; } /** * Find and remove block with specific name * * @param blocks List of ParamOutputBlocks to search * @param name Name of block to find * @return ParamOutputBlock with specified name */ private NamedOutputBlockContext findAndRemoveBlock(List<NamedOutputBlockContext> blocks, String name) { if(blocks == null) { return null; } Iterator<NamedOutputBlockContext> blockIter = blocks.iterator(); while (blockIter.hasNext()) { NamedOutputBlockContext block = blockIter.next(); String blockName = name(block); if(name.equals(blockName)) { blockIter.remove(); return block; } } return null; } /** * Find and remove named value with specific name * * @param namedValues List of NamedValues to search * @param name Name of value to find * @return Expression for value with specified name */ private ExpressionContext findAndRemoveValue(List<NamedValueContext> namedValues, String name) { Iterator<NamedValueContext> namedValueIter = namedValues.iterator(); while (namedValueIter.hasNext()) { NamedValueContext namedValue = namedValueIter.next(); if (name.equals(value(namedValue.name))) { namedValueIter.remove(); return namedValue.expr; } } return null; } /** * Evaluate an expression into a value * * @param expr Expression to evaluate * @return Result of expression evaluate */ private Object eval(ExpressionContext expr) { Object res = expr; while(res instanceof ExpressionContext) res = ((ExpressionContext)res).accept(visitor); if(res instanceof LValue) res = ((LValue) res).get(expr); return res; } private Object eval(VariableRefContext ref) { return currentScope.ref(name(ref), ref); } private LValue eval(LValueRefContext ref) { return (LValue) ref.accept(visitor); } /** * Evaluates a list of expressions into their values * * @param expressions List of expressions to evaluate * @return List of related expression values */ private List<Object> eval(Iterable<ExpressionContext> expressions) { List<Object> results = new ArrayList<Object>(); for (ExpressionContext expression : expressions) { results.add(eval(expression)); } return results; } /** * Execute a statement block until the first * return statement is reached. * * @param block StatementBlock to evaluate * @return Result of expression evaluate */ private Object exec(StatementContext statement) { if (statement == null) return null; return statement.accept(visitor); } private Object exec(BlockStatementContext statement) { if (statement == null) return null; return statement.accept(visitor); } /** * Evaluate an expression into a boolean value. * * Conversion Rules: Boolean => Boolean String => !val.isEmpty() Collection => * !val.isEmpty() <Else> => val != null * * @param expr * Expression to evaluate * @return Result of expression as boolean using conversion rules */ private boolean evalBoolean(ExpressionContext expr) { return operands.toBoolean(eval(expr)); } /** * Evaluate expression into an Integer value * * @param expr * Expression to evaluate * @return Result of expression as integer * @throws ExecutionException * when expression doesn't evaluate to an integer */ private Integer evalInteger(ExpressionContext expr) { return operands.toInteger(eval(expr)); } /** * Evaluate expression into a Collection value * * @param expr * Expression to evaluate * @return Result of expression as collection * @throws ExecutionException * when expression doesn't evaluate to a collection */ private Collection<?> evalCollection(ExpressionContext expr) { return operands.toCollection(eval(expr)); } /** * Retrieves location information for a model object * * @param object Model object to locate * @return Location for the provided model object */ ExecutionLocation getLocation(ParserRuleContext object) { Token start = object.getStart(); return new ExecutionLocation(currentScope.declaringEnvironment.path, start.getLine(), start.getCharPositionInLine() + 1); } void output(Object val) { if(val == null) return; try { currentEnvironment.out.append(val.toString()); } catch (IOException e) { ExecutionLocation location = null; if (val instanceof ParserRuleContext) location = getLocation((ParserRuleContext) val); throw new ExecutionException(e, location); } } } class NullWriter extends Writer { public void close() { // blank } public void flush() { // blank } public void write(char[] cbuf, int off, int len) { // blank } }